import numpy as np
import pandas as pd
from itertools import product

import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

Calculation

## starting parameters
# the first and last layers is air (non-lossy and set to 0 thickness) 
# -- assuming we are interested in optical responses of the structure in the middle   
thickness     = np.array([0, 1000, 0])
ep            = np.array([1, 4, 1])



# be consistent units e.g. if your wavelength is in nm, you film thickness should be in nm
wavelength    = 500
n             = np.sqrt(ep)
k             = 2*np.pi*n/wavelength



# define angle of incidence
theta_inc     = 0
theta_inc_rad = np.deg2rad(theta_inc)



# wavenumber in incidence material
k_inc         = k[0]

# tagent component of wavenumber
k_tan         = k_inc*np.sin(theta_inc_rad)

# angle of incidence in all layers
theta         = np.arcsin(k_tan/k)

# propagating component of wavenumber in all layers
k_prop        = np.sqrt(np.square(k) - np.square(k_tan))
# or -- k_prop        = k*np.cos(theta)

# phase gain in all layers
delta         = k*np.cos(theta)*thickness
def transfer_matrix_layer(delta_now, n1, n2, theta1, theta2):
    ''' 
    Calculate transfer-matrix of each layer.
    '''
    
    ## TE component
    # propagation matrix
    prp_te = np.array( [[np.exp(-1j*delta_now), 0], [0, np.exp(1j*delta_now)]] )
    
    # reflection matrix
    r_te   = (n1*np.cos(theta1) - n2*np.cos(theta2)) / (n1*np.cos(theta1) + n2*np.cos(theta2))
    t_te   = 2*n1*np.cos(theta1) / (n1*np.cos(theta1) + n2*np.cos(theta2))
    rlc_te = 1 / t_te * np.array( [[1, r_te], [r_te, 1]] )
    
    # combined matrix on that layer
    m_te   = np.matmul(prp_te, rlc_te)
    
    
    
    ## TM component
    # propagation matrix -- same as TE
    prp_tm = np.array( [[np.exp(-1j*delta_now), 0], [0, np.exp(1j*delta_now)]] )
    
    # reflection matrix
    r_tm   = (n2*np.cos(theta1) - n1*np.cos(theta2)) / (n2*np.cos(theta1) + n1*np.cos(theta2))
    t_tm   = 2*n1*np.cos(theta1)/(n2*np.cos(theta1) + n1*np.cos(theta2))
    rlc_tm = 1 / t_tm * np.array( [[1, r_tm], [r_tm, 1]])
    
    # combined matrix on that layer
    m_tm   = np.matmul(prp_tm, rlc_tm)
    
    
    return m_te, m_tm
def transfer_matrix(delta, n, theta):
    ''' 
    Calculate the combined transfer-matrix of wave propagation.
    '''
    # starting matrices
    m_te_all = np.identity(2)
    m_tm_all = np.identity(2)

    # combine transfer matrix of all layers
    for delta_now, n1, n2, theta1, theta2 in zip(delta[:-1], n[:-1], n[1:], theta[:-1], theta[1:]):
        
        # transfer matrix of each layer
        m_te, m_tm = transfer_matrix_layer(delta_now, n1, n2, theta1, theta2)
        
        # multiply to the previous matrix
        m_te_all   = np.matmul(m_te_all, m_te)
        m_tm_all   = np.matmul(m_tm_all, m_tm)
        
    
    return m_te_all, m_tm_all
def reflectance_transmitance(delta, n, theta):
    ''' 
    Get optical reflactance and transmittance (TE and TM).
    Valid when first and last layers are non-lossy materials.
    '''
    # get m_te_all and m_tm_all
    m_te_all, m_tm_all = transfer_matrix(delta, n, theta)


    # TE reflection/transmission coefficients
    reflect_te = m_te_all[1,0] / m_te_all[0,0]
    transmt_te = 1 / m_te_all[0,0]
    # TE reflectance and transmittance
    r_power_te = np.abs(reflect_te)**2
    t_power_te = np.abs(transmt_te)**2 * np.real( n[-1]*np.cos(theta[-1]) ) / np.real( n[0]*np.cos(theta[0]) )
    
    
    # TM reflection/transmission coefficients
    reflect_tm = m_tm_all[1,0] / m_tm_all[0,0]
    transmt_tm = 1 / m_tm_all[0,0]
    # TM reflectance and transmittance
    r_power_tm = np.abs(reflect_tm)**2
    t_power_tm = np.abs(transmt_tm)**2 * np.real( n[-1]*np.cos(theta[-1]) ) / np.real( n[0]*np.cos(theta[0]) )
    
    
    return r_power_te, t_power_te, r_power_tm, t_power_tm
def get_params(thickness, n, wavelength, theta_inc):
    '''
    Get varying parameters due to changing wavelength and theta_inc.
    '''
    # calculate wavenumber in all layers
    k             = 2*np.pi*n/wavelength

    # wavenumber in incidence material
    k_inc         = k[0]

    # tagent component of wavenumber
    theta_inc_rad = np.deg2rad(theta_inc)
    k_tan         = k_inc*np.sin(theta_inc_rad)

    
    
    # angle of incidence in all layers
    theta         = np.arcsin(k_tan/k)

    # propagating component of wavenumber in all layers
#     k_prop        = np.sqrt(np.square(k) - np.square(k_tan))
#      or -- k_prop        = k*np.cos(theta)

    # phase gain in all layers
    delta         = k*np.cos(theta)*thickness
    
    return delta, theta
def get_reflectance_transmittance(thickness, n, wavelength, theta_inc):
    '''
    Get optical reflactance and transmittance (TE, TM, and total).
    '''
    
    delta, theta                                   = get_params(thickness, n, wavelength, theta_inc)
    r_power_te, t_power_te, r_power_tm, t_power_tm = reflectance_transmitance(delta, n, theta)
    
    return r_power_te, t_power_te, r_power_tm, t_power_tm, (r_power_te + r_power_tm)/2, (t_power_te + t_power_tm)/2

Test plotting with seaborn

wavelength_range = range(400, 701, 1)
theta_inc_range  = range(0, 90, 1)
excitation_df = pd.DataFrame(list(product(wavelength_range, theta_inc_range)), 
                             columns=['wavelength', 'theta_inc']
                            )
response_df = pd.concat([excitation_df, 
                         pd.DataFrame(excitation_df.apply(lambda x: 
                                                          get_reflectance_transmittance(thickness, n, x.wavelength, x.theta_inc)
                                                          , axis=1
                                                         ).tolist(),
                                      columns = ['R_TE', 'T_TE', 'R_TM', 'T_TM', 'R_Total', 'T_Total']
                                     )
                        ],
                        axis=1
                       )
fig, axes = plt.subplots(3, 2, figsize=(15, 12), sharex=True, sharey=True)

for value, ax in zip(['R_TE', 'T_TE', 'R_TM', 'T_TM', 'R_Total', 'T_Total'], axes.flatten()):
    sns.heatmap(pd.pivot_table(response_df, 
                               values=value, 
                               index='theta_inc', 
                               columns='wavelength'
                              ), 
                annot=False, ax=ax
               )
    ax.set_title(value)
    ax.invert_yaxis()

fig.tight_layout()

Plotly

import plotly.graph_objs as go
from ipywidgets import interactive, Label, HTML, HBox, VBox
from plotly.subplots import make_subplots

Practice using widgets in plotting

########################################
f_R_lambda = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.theta_inc==min(theta_inc_range), 'wavelength'], 
                     y=response_df.loc[response_df.theta_inc==min(theta_inc_range), mode], 
                     name=mode
                      ) for mode in ['R_TE', 'R_TM', 'R_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)

        
f_T_lambda = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.theta_inc==min(theta_inc_range), 'wavelength'], 
                     y=response_df.loc[response_df.theta_inc==min(theta_inc_range), mode], 
                     name=mode
                      ) for mode in ['T_TE', 'T_TM', 'T_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)


def update_RT_theta(theta_inc):
    for selected_data, mode in zip(f_R_lambda.data, ['R_TE', 'R_TM', 'R_Total']):
        selected_data.y = response_df.loc[response_df.theta_inc==theta_inc, mode]
    for selected_data, mode in zip(f_T_lambda.data, ['T_TE', 'T_TM', 'T_Total']):
        selected_data.y = response_df.loc[response_df.theta_inc==theta_inc, mode]
        
########################################

f_R_theta = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.wavelength==min(wavelength_range), 'theta_inc'], 
                     y=response_df.loc[response_df.wavelength==min(wavelength_range), mode], 
                     name=mode
                      ) for mode in ['R_TE', 'R_TM', 'R_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)

        
f_T_theta = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.wavelength==min(wavelength_range), 'theta_inc'], 
                     y=response_df.loc[response_df.wavelength==min(wavelength_range), mode], 
                     name=mode
                      ) for mode in ['T_TE', 'T_TM', 'T_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)


def update_RT_wavelength(wavelength):
    for selected_data, mode in zip(f_R_theta.data, ['R_TE', 'R_TM', 'R_Total']):
        selected_data.y = response_df.loc[response_df.wavelength==wavelength, mode]
    for selected_data, mode in zip(f_T_theta.data, ['T_TE', 'T_TM', 'T_Total']):
        selected_data.y = response_df.loc[response_df.wavelength==wavelength, mode]
        

########################################

theta_slider  = interactive(update_RT_theta, theta_inc=(1, 90, 1))
lambda_slider = interactive(update_RT_wavelength, wavelength=(400, 700, 10))

hb1 = HBox((f_R_lambda, f_T_lambda))
vb1 = VBox((theta_slider, hb1))
vb1.layout.align_items = 'center'

hb2 = HBox((f_R_theta, f_T_theta))
vb2 = VBox((lambda_slider, hb2))
vb2.layout.align_items = 'center'

vb3 = VBox((vb1, vb2))
vb3
figs_R = []
figs_T = []


for mode in ['R_TE', 'R_TM', 'R_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig = go.FigureWidget(data=
                          go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                     colorscale='hot',
                                     name=mode
                                    ),
                          layout=go.Layout(width=500, height=300, title=mode)
                         )
    fig.update_layout(title={'x': 0.5, 'xanchor': 'center'})
    figs_R = figs_R + [fig]

    
for mode in ['T_TE', 'T_TM', 'T_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig = go.FigureWidget(data=
                          go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                     colorscale='hot',
                                     name=mode
                                    ),
                          layout=go.Layout(width=500, height=300, title=mode)
                         )
    fig.update_layout(title={'x': 0.5, 'xanchor': 'center'})
                          
    figs_T = figs_T + [fig]


vb1 = VBox(figs_R)
vb2 = VBox(figs_T)
hb  = HBox((vb1, vb2))
hb
########################################

fig = go.FigureWidget(make_subplots(rows=1, cols=2, subplot_titles=("Reflectance", "Transmittance")))

fig.update_xaxes(title_text='Wavelength (nm)')
fig.update_yaxes(title_text='Angle of Incidence (degree)')

fig.update_layout(height=400, width=800)


for mode in ['R_TE', 'R_TM', 'R_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                             colorscale='hot',
                             name=mode,
                             showscale=True
                            ),
                  row=1, col=1
                 )
    
for mode in ['T_TE', 'T_TM', 'T_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                             colorscale='hot',
                             name=mode,
                             showscale=False
                            ),
                  row=1, col=2
                 )

fig.update_layout(
    updatemenus=[
        go.layout.Updatemenu(
            name='Optical Reflectance and Transmittance',
            active=0,
            x=0.5,
            xanchor="center",
            y=1.2,
            yanchor="top",
            buttons=list([
                dict(label="Total",
                     method="update",
                     args=[{"visible": [False, False, True, False, False, True]},
#                            {"title": "TM Response"}
                          ]),
                dict(label="TE",
                     method="update",
                     args=[{"visible": [True, False, False, True, False, False]},
#                            {"title": "Total Response"}
                          ]),
                dict(label="TM",
                     method="update",
                     args=[{"visible": [False, True, False, False, True, False]},
#                            {"title": "TE Response"}
                          ]),
            ]),
        )
    ])

fig.show()
########################################

fig = go.FigureWidget(make_subplots(rows=1, cols=2, subplot_titles=("Reflectance", "Transmittance")))

for mode in ['R_TE', 'R_TM', 'R_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                 colorscale='hot',
                                 name=mode
                                ),
                  row=1, col=1
                 )
    
for mode in ['T_TE', 'T_TM', 'T_Total']:
    heatmap = pd.pivot_table(response_df, values=mode, index='theta_inc', columns='wavelength')
    fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                 colorscale='hot',
                                 name=mode,
                                ),
                  row=1, col=2
                 )

fig.update_layout(
    updatemenus=[
        go.layout.Updatemenu(
            name='Optical Reflectance and Transmittance',
            active=0,
            x=0.5,
            xanchor="center",
            y=1.2,
            yanchor="top",
            buttons=list([
                dict(label="Total",
                     method="update",
                     args=[{"visible": [False, False, True, False, False, True]},
#                            {"title": "TM Response"}
                          ]),
                dict(label="TE",
                     method="update",
                     args=[{"visible": [True, False, False, True, False, False]},
#                            {"title": "Total Response"}
                          ]),
                dict(label="TM",
                     method="update",
                     args=[{"visible": [False, True, False, False, True, False]},
#                            {"title": "TE Response"}
                          ]),
            ]),
        )
    ])

########################################

f_R_lambda = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.theta_inc==min(theta_inc_range), 'wavelength'], 
                     y=response_df.loc[response_df.theta_inc==min(theta_inc_range), mode], 
                     name=mode
                      ) for mode in ['R_TE', 'R_TM', 'R_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)

        
f_T_lambda = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.theta_inc==min(theta_inc_range), 'wavelength'], 
                     y=response_df.loc[response_df.theta_inc==min(theta_inc_range), mode], 
                     name=mode
                      ) for mode in ['T_TE', 'T_TM', 'T_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)


def update_RT_theta(theta_inc):
    for selected_data, mode in zip(f_R_lambda.data, ['R_TE', 'R_TM', 'R_Total']):
        selected_data.y = response_df.loc[response_df.theta_inc==theta_inc, mode]
    for selected_data, mode in zip(f_T_lambda.data, ['T_TE', 'T_TM', 'T_Total']):
        selected_data.y = response_df.loc[response_df.theta_inc==theta_inc, mode]
        
        
########################################

f_R_theta = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.wavelength==min(wavelength_range), 'theta_inc'], 
                     y=response_df.loc[response_df.wavelength==min(wavelength_range), mode], 
                     name=mode
                      ) for mode in ['R_TE', 'R_TM', 'R_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)

        
f_T_theta = go.FigureWidget(
    data=[go.Scatter(x=response_df.loc[response_df.wavelength==min(wavelength_range), 'theta_inc'], 
                     y=response_df.loc[response_df.wavelength==min(wavelength_range), mode], 
                     name=mode
                      ) for mode in ['T_TE', 'T_TM', 'T_Total']
           ],
    layout=go.Layout(width=500, legend_orientation='h', legend={'x':0,'y':1.1})
)


def update_RT_wavelength(wavelength):
    for selected_data, mode in zip(f_R_theta.data, ['R_TE', 'R_TM', 'R_Total']):
        selected_data.y = response_df.loc[response_df.wavelength==wavelength, mode]
    for selected_data, mode in zip(f_T_theta.data, ['T_TE', 'T_TM', 'T_Total']):
        selected_data.y = response_df.loc[response_df.wavelength==wavelength, mode]


#########################################


theta_slider  = interactive(update_RT_theta, theta_inc=(1, 90, 1))
lambda_slider = interactive(update_RT_wavelength, wavelength=(400, 700, 10))

hb1 = HBox((f_R_lambda, f_T_lambda))
vb1 = VBox((theta_slider, hb1))
vb1.layout.align_items = 'center'

hb2 = HBox((f_R_theta, f_T_theta))
vb2 = VBox((lambda_slider, hb2))
vb2.layout.align_items = 'center'

vb3 = VBox((fig, vb1, vb2))
vb3

Use widgets to get user input

def get_n_and_thickness(n_real, n_imag, thickness):
    '''
    Get user inputs of refractive indices (real and imaginary) and layer thicknesses (nm).
    '''
    
    # convert to arguments to lists
    n_real_list    = [float(n.strip()) for n in n_real.split(';')]
    n_imag_list    = [float(n.strip()) for n in n_imag.split(';')]
    thickness_list = [float(thickness.strip()) for thickness in thickness.split(';')]

    # check number of input layers
    if len(n_real_list) != len(n_imag_list):
        raise Exception('Numbers of real and imaginary part of refractive indices are not equal.')
    if len(n_real_list) != len(thickness_list):
        raise Exception('Numbers of refractive indices and thicknesses are not equal.')
    
    # get n and thickness
    n              = np.array([(n_real + 1J*n_imag) for n_real, n_imag in zip(n_real_list, n_imag_list)])
    thickness      = np.array(thickness_list)
    
    # pad front and back with n=1 and thickness=0 (air)
    n              = np.pad(n,         1, 'constant', constant_values=1)
    thickness      = np.pad(thickness, 1, 'constant', constant_values=0)
    
    
    return(n, thickness)
def get_excitation_conidtion_df():
    '''
    Excitation conditions to initialize dataframe for calculation.
    Currently wavelength_range and theta_inc are fixed -- will allow user to update in future version.
    '''
    # currently wavelength_range and theta_inc are fixed -- will allow user to update in future version
    wavelength_range = range(400, 701, 1)
    theta_inc_range  = range(0, 90, 1)
    
    excitation_df = pd.DataFrame(list(product(wavelength_range, theta_inc_range)),
                                 columns=['wavelength', 'theta_inc']
                                )
    
    return excitation_df
def get_RT_all_conds(excitation_df, n, thickness):
    '''
    Calculate reflectance and transmittance for all excitation conditions and return RT dataframe.
    '''
    
    response_df = pd.DataFrame(excitation_df.apply(lambda x: 
                                                   get_reflectance_transmittance(thickness, 
                                                                                 n, 
                                                                                 x.wavelength, 
                                                                                 x.theta_inc
                                                                                )
                                                   , axis=1
                                                  ).tolist(),
                               columns = ['R_TE', 'T_TE', 'R_TM', 'T_TM', 'R_Total', 'T_Total']
                              )
    
    RT_df = pd.concat([excitation_df, response_df], axis=1)
    
    return RT_df
def plot_heatmaps(RT_df):
    '''
    Return heatmaps of reflectance and transmittance.
    Also with option to select excitation mode to display (TE, TM, Total).
    '''
    
    fig = go.FigureWidget(make_subplots(rows=1, cols=2, subplot_titles=("Reflectance", "Transmittance")))
    
    fig.update_xaxes(title_text='Wavelength (nm)')
    fig.update_yaxes(title_text='Angle of Incidence (degree)')

    fig.update_layout(height=400)#, width=800)

    for mode in ['R_TE', 'R_TM', 'R_Total']:
        heatmap = pd.pivot_table(RT_df, values=mode, index='theta_inc', columns='wavelength')
        fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                 colorscale='hot',
                                 name=mode,
                                 zmin=0, zmax=1,
                                 showscale=False,
                                ),
                      row=1, col=1
                     )

    for mode in ['T_TE', 'T_TM', 'T_Total']:
        heatmap = pd.pivot_table(RT_df, values=mode, index='theta_inc', columns='wavelength')
        fig.add_trace(go.Heatmap(z=heatmap.values,x=heatmap.columns,y=heatmap.index,
                                 colorscale='hot',
                                 name=mode,
                                 zmin=0, zmax=1,
                                 showscale=True
                                ),
                      row=1, col=2
                     )

    fig.update_layout(
        updatemenus=[
            go.layout.Updatemenu(
                name='Optical Reflectance and Transmittance',
                active=0,
                x=0.5,
                xanchor="center",
                y=1.2,
                yanchor="top",
                buttons=list([
                    dict(label="Total",
                         method="update",
                         args=[{"visible": [False, False, True, False, False, True]},
#                                {"title": "TM Response"}
                              ]),
                    dict(label="TE",
                         method="update",
                         args=[{"visible": [True, False, False, True, False, False]},
#                                {"title": "Total Response"}
                              ]),
                    dict(label="TM",
                         method="update",
                         args=[{"visible": [False, True, False, False, True, False]},
#                                {"title": "TE Response"}
                              ]),
                ]),
            )
        ])
    
    
    #return fig
    return fig
def update_layers_cal(n_real, n_imag, thickness):
    '''
    Get user specified information of thin film layers (refractive indeices and thicknesses).
    Plot heatmaps of reflectance and transmittance.
    Return RT dataframe for all excitation conditions.
    '''
        
    # get user specified information of thin film layers
    n, thickness  = get_n_and_thickness(n_real, n_imag, thickness)
    
    # define excitation condition -- currently wavelength 400-700nm and theta_inc 0-89 deg
    excitation_df = get_excitation_conidtion_df()
    
    # get RT dataframe 
    RT_df         = get_RT_all_conds(excitation_df, n, thickness)
    
    # plot the RT heatmaps
    plot_heatmaps(RT_df).show()

    # return RT dataframe in form of update_layers.result
    return RT_df
# test getting user input and plotting RT heatmaps 
update_layers = interactive(update_layers_cal, 
                            {'manual': True, 'manual_name': 'Update layers'}, 
                            n_real='Real{n}', 
                            n_imag='Imag{n}',
                            thickness='Thickness (nm)'
                           )

update_layers
update_layers.result

Complete flow from user input, to RT heatmaps, to RT by wavelength/theta_inc

# UI to update thin film layers
update_layers = interactive(update_layers_cal, 
                            {'manual': True, 'manual_name': 'Update layers'}, 
                            n_real='Real{n}', 
                            n_imag='Imag{n}',
                            thickness='Thickness (nm)'
                           )


# fixed wavelength for now 
# need to also update get_excitation_conidtion_df() when user type in these two parameters
wavelength_range = range(400, 701, 1)
theta_inc_range  = range(0, 90, 1)


# set initial plots before user provides information of thin film layers
if update_layers.result is None:
    
    #### RT across wavelength range
    # Reflectance across range of wavelength -- initially all 0
    f_R_lambda = go.FigureWidget(
        data=[go.Scatter(x=np.array(wavelength_range), 
                         y=np.zeros(len(wavelength_range)), 
                         name=mode
                          ) for mode in ['R_TE', 'R_TM', 'R_Total']
               ],
        layout=go.Layout(width=500, 
                         legend_orientation='h', legend={'x':0,'y':1.1}, 
                         xaxis_title='Wavelength (nm)',
                         yaxis_title='Reflectance (a.u.)', 
                         yaxis={'range':[-0.1, 1.1]}
                        )
    )

    # Transmittance across range of wavelength -- initially all 1
    f_T_lambda = go.FigureWidget(
        data=[go.Scatter(x=np.array(wavelength_range), 
                         y=np.ones(len(wavelength_range)), 
                         name=mode
                          ) for mode in ['T_TE', 'T_TM', 'T_Total']
               ],
        layout=go.Layout(width=500,
                         legend_orientation='h', 
                         legend={'x':0,'y':1.1}, 
                         xaxis_title='Wavelength (nm)',
                         yaxis_title='Transmittance (a.u.)', 
                         yaxis={'range':[-0.1, 1.1]}
                        )
    )
    
    
    
    #### RT across theta_inc range
    # Reflectance across range of angle of incidencec -- initially all 0
    f_R_theta = go.FigureWidget(
        data=[go.Scatter(x=np.array(theta_inc_range), 
                         y=np.zeros(len(theta_inc_range)), 
                         name=mode
                          ) for mode in ['R_TE', 'R_TM', 'R_Total']
               ],
        layout=go.Layout(width=500, 
                         legend_orientation='h', legend={'x':0,'y':1.1}, 
                         xaxis_title='Angle of Incidence (degree)', 
                         yaxis_title='Reflectance (a.u.)',
                         yaxis={'range':[-0.1, 1.1]}
                        )
    )

    # Transmittance across range of angle of incidencec -- initially all 1
    f_T_theta = go.FigureWidget(
        data=[go.Scatter(x=np.array(theta_inc_range), 
                         y=np.ones(len(theta_inc_range)), 
                         name=mode
                          ) for mode in ['T_TE', 'T_TM', 'T_Total']
               ],
        layout=go.Layout(width=500, 
                         legend_orientation='h', legend={'x':0,'y':1.1}, 
                         xaxis_title='Angle of Incidence (degree)', 
                         yaxis_title='Transmittance (a.u.)',
                         yaxis={'range':[-0.1, 1.1]}
                        )
    )

    
    
########################################
# function to plot RT at different theta_inc
def update_RT_theta(theta_inc):
    if update_layers.result is None:
        pass
    else:
        RT_df = update_layers.result
        for selected_data, mode in zip(f_R_lambda.data, ['R_TE', 'R_TM', 'R_Total']):
            selected_data.y = RT_df.loc[RT_df.theta_inc==theta_inc, mode]
        for selected_data, mode in zip(f_T_lambda.data, ['T_TE', 'T_TM', 'T_Total']):
            selected_data.y = RT_df.loc[RT_df.theta_inc==theta_inc, mode]
            
        
# UI to update theta_inc
theta_slider  = interactive(update_RT_theta, theta_inc=(1, 90, 1))



########################################
# function to plot RT at different wavelength
def update_RT_wavelength(wavelength):
    if update_layers.result is None:
        pass
    else:
        RT_df = update_layers.result
        for selected_data, mode in zip(f_R_theta.data, ['R_TE', 'R_TM', 'R_Total']):
            selected_data.y = RT_df.loc[RT_df.wavelength==wavelength, mode]
        for selected_data, mode in zip(f_T_theta.data, ['T_TE', 'T_TM', 'T_Total']):
            selected_data.y = RT_df.loc[RT_df.wavelength==wavelength, mode]
        

# UI to update lambda
lambda_slider = interactive(update_RT_wavelength, wavelength=(400, 700, 10))



########################################



# header messages
header_text      = 'Interactive heatmaps and plots of optical reflectance and transmittance through thin films using transfer-matrix calculation.'
header           = HTML(value='<{size}>{text}</{size}>'.format(text=header_text, size='h2'))
description_text = 'Assume light propagates from <u><b>air</b></u> to <b>specified thin film layers</b>, then exits to <u><b>air</b></u>.'
description      = HTML(value='<{size}>{text}</{size}>'.format(text=description_text, size='h3'))



# description to update thin film layers
update_layers_inst      = 'Update layers and perform calculation. Note that it could take a while.'
update_layers_note      = HTML(value='<{size}>{text}</{size}>'.format(text=update_layers_inst, size='h3'))

update_layers_n_real    = 'List real part of refractive indices of materials from the front to the back of a thin film stack separated by <b>;</b> (e.g. 1.5; 1.33; 1.5).'
update_layers_n_imag    = 'List imaginary part of refractive indices of materials separated by <b>;</b> (e.g. 0.1; 0; 0.05, positive and negative values for absorber and amplifier, respectively).'
update_layers_thickness = 'List thickness of materials in <b>nanometer</b> separated by <b>;</b> (e.g. 500; 800; 500).'
update_layers_text      = f'{update_layers_n_real}<br>{update_layers_n_imag}<br>{update_layers_thickness}'
update_layers_html      = HTML(value=update_layers_text)



# RT across range of wavelength at difference theta_inc
theta_slide_text  = 'Move <b>theta_inc slider</b> to see reflectance and transmittance at different angle of incidence.'
theta_slide_html  = HTML(value='<{size}>{text}</{size}>'.format(text=theta_slide_text, size='h3'))
hb1               = HBox((f_R_lambda, f_T_lambda))
vb1               = VBox((theta_slide_html, theta_slider, hb1))



# RT across range of theta_inc at difference wavelength
lambda_slide_text = 'Move <b>wavelength slider</b> to see reflectance and transmittance at different wavelength.'
lambda_slide_html = HTML(value='<{size}>{text}</{size}>'.format(text=lambda_slide_text, size='h3'))
hb2               = HBox((f_R_theta, f_T_theta))
vb2               = VBox((lambda_slide_html, lambda_slider, hb2))



output = VBox((header,
               description,
               Label('#'*100),
               update_layers_note,
               update_layers_html,
               update_layers,
               Label('#'*100),
               vb1,
               Label('#'*100),
               vb2
              )
             )

output